<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>


  <script>
    const TriggerType = Object.freeze({
      SET: 'SET',
      ADD: 'ADD',
      DELETE: 'DELETE'
    })

    const ITERATE_KEY = Symbol()

    let activeEffect

    const effectStack = []

    function effect (fn, options = {}) {

      const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        effectStack.push(effectFn)
        const res = fn()
        effectStack.pop()

        activeEffect = effectStack[effectStack.length - 1]

        return res
      }


      effectFn.options = options
      effectFn.deps = []

      if (!effectFn.options.lazy) {
        effectFn()
      }

      return effectFn
    }

    function cleanup (effectFn) {
      for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
      }

      effectFn.deps.length = 0
    }

    const bucket = new WeakMap()

    const track = (target, key) => {

      // 禁止追踪则直接返回
      if(!activeEffect || !shouldTrack) return;

      if (!activeEffect) {
        return target[key]
      }

      // 1. 获取当前对象对应的依赖集合
      let depsMap = bucket.get(target)

      if (!depsMap) {
        depsMap = new Map()
        bucket.set(target, depsMap)
      }

      // 2. 获取当前属性对应的依赖集合
      let deps = depsMap.get(key)

      if (!deps) {
        deps = new Set()
        depsMap.set(key, deps)
      }

      deps.add(activeEffect)

      activeEffect.deps.push(deps)

    }

    const trigger = (target, key, type, newValue) => {
      // 1. 获取当前对象对应的依赖集合
      const depsMap = bucket.get(target)
      if (!depsMap) return

      // 2. 获取当前属性对应的依赖集合
      const effects = depsMap.get(key)

      // 3. 执行依赖函数
      const effectsToRun = new Set()
      effects && effects.forEach((fn) => {
        if (activeEffect !== fn) { // 避免自己执行自己 无限递归爆栈
          effectsToRun.add(fn)
        }
      })

      if (type === TriggerType.ADD || type === TriggerType.DELETE) { // 如果是添加 或 删除
        const iterateEffects = depsMap.get(ITERATE_KEY)
        iterateEffects && iterateEffects.forEach((effectFn) => {
          if (activeEffect !== effectFn) { // 避免自己执行自己 无限递归爆栈
            effectsToRun.add(effectFn)
          }
        })
      }

      // 当操作类型是 添加  且代理对象是数组的时候 取出并执行和length属性相关联的副作用函数
      if (type === TriggerType.ADD && Array.isArray(target)) {
        const lengthEffects = depsMap.get('length')
        lengthEffects && lengthEffects.forEach((effectFn) => {
          if (activeEffect !== effectFn) { // 避免自己执行自己 无限递归爆栈
            effectsToRun.add(effectFn)
          }
        })
      }


      // 数组 且 操作length
      if (Array.isArray(target) && key === 'length') {

        depsMap.forEach((effects, key) => {
          // 当索引大于或等于 新的length值 则取出并执行和索引相关的副作用函数
          // 疑惑点key: 当访问length时就是length 当通过索引访问元素的时候就是索引值
          if (key >= newValue) {

            effects.forEach((effectFn) => {
              if (activeEffect !== effectFn) { // 避免自己执行自己 无限递归爆栈
                effectsToRun.add(effectFn)
              }
            })
          }
        })
      }


      effectsToRun && effectsToRun.forEach((effectFn) => {
        // 如果存在调度器 则调用调度器 把函数作为参数传入
        if (effectFn.options.scheduler) {
          effectFn.options.scheduler(effectFn)
        } else {
          effectFn()
        }
      })
    }


    // 重寫 數組方法
    const arrayInstrumentations = {};

    ['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
      const originMethod = Array.prototype[method]
      arrayInstrumentations[method] = function (...args) {
        // this是代理对象，先在代理对象中查找
        let res = originMethod.apply(this, args)

        //如果返回false 则表示 找不到， 通过this.raw 拿到原数组  再去其中查找并更新res值
        if (res === false) {
          res = originMethod.apply(this.raw, args)
        }

        return res
      }
    })

    // 标记变量 代表是否进行追踪 默认值为true 允许追踪
    let shouldTrack = true;

    ['push','pop','shift','unshift','splice'].forEach(method=>{
      // 获取原始的数组方法
      const originMethod = Array.prototype[method]
      arrayInstrumentations[method] = function (...args) {
        // 调用原始方法的时候禁止追踪
        shouldTrack = false
        let res = originMethod.apply(this, args)
        
        // 调用原始方法后 就恢复原来行为 允许追踪
        shouldTrack=true

        return res 
      }

    })


    function createReactive (obj, isShallow = false, isReadonly = false) {
      return new Proxy(obj, {
        get (target, key, receiver) {

          // 代理对象可以通过 raw 属性访问原始数据
          if (key === 'raw') {
            return target
          }

          // 如果操作对象是数组 且 key存在于arrayInstrumentations 
          // 则返回arrayInstrumentations中对应的函数
          if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
            return Reflect.get(arrayInstrumentations, key, receiver)
          }

          if (!isReadonly && typeof key !== 'symbol') {
            track(target, key)
          }

          const res = Reflect.get(target, key, receiver)

          // 只读则不需要追踪响应式
          if (!isReadonly) {
            track(target, key)
          }

          // 浅响应 直接返回对象
          if (isShallow) {
            return res
          }

          // 如果是对象则继续递归 返回响应式数据
          if (typeof res === 'object' && res !== null) {
            return isReadonly ? readonly(res) : reactive(res)
          }

          return res
        },

        set (target, key, newValue, receiver) {

          if (isReadonly) {
            console.warn(`${key}属性是只读的`)
            return true
          }

          // 获取旧值
          const oldValue = target[key]

          // 如果属性不存在则认为是添加
          const type = Array.isArray(target) ?
            // 如果代理对象是数组 则根据索引判断是修改还是添加
            Number(key) < target.length ? TriggerType.SET : TriggerType.ADD
            : Object.prototype.hasOwnProperty.call(target, key) ? TriggerType.SET : TriggerType.ADD

          const res = Reflect.set(target, key, newValue, receiver)


          // 代理对象可以通过 raw 属性访问原始数据
          if (target === receiver.raw) {
            // 考虑新旧值变化问题，且NAN 问题
            // NaN !== NaN  true
            // NaN === NaN  false
            if (newValue !== oldValue && (oldValue === oldValue || newValue === newValue)) {
              trigger(target, key, type, newValue)
            }
          }

          return res
        },

        deleteProperty (target, key) {

          if (isReadonly) {
            console.warn(`${key}属性是只读的`)
            return true
          }

          const hadKey = Object.prototype.hasOwnProperty.call(target, key)
          const res = Reflect.deleteProperty(target, key)
          if (res && hadKey) {
            trigger(target, key, TriggerType.DELETE)
          }
          return res
        },

        has (target, key) {
          track(target, key)
          return Reflect.has(target, key)
        },

        ownKeys (target) {
          // 如果操作目标target 是数组，其实就是对length属性进行相关操作，就使用length作为key建立联系
          track(target, Array.isArray(target) ? 'length' : ITERATE_KEY)
          return Reflect.ownKeys(target)
        }

      })
    }

    // 定义 一个map 实例， 存储原始对象到代理对象的映射
    const reactiveMap = new Map()

    function reactive (obj) {

      // 如果这个对象已经被代理过了，就直接返回代理对象
      const existProxy = reactiveMap.get(obj)
      if (existProxy) return existProxy

      // 否则，创建新的代理对象
      const proxy = createReactive(obj)
      // 存储到map中， 避免重复创建
      reactiveMap.set(obj, proxy)

      return proxy
    }

    function shallowReactive (obj) {
      return createReactive(obj, true)
    }

    function readonly (obj) {
      return createReactive(obj, false, true)
    }

    function shallowReadonly (obj) {
      return createReactive(obj, true, true)
    }

    const arr = reactive([])

    effect(() => {

      // console.log('%c [  ]-231', 'font-size:13px; background:pink; color:#bf2c9f;', arr.length);

      // console.log('%c [ effect ]-247', 'font-size:13px; background:pink; color:#bf2c9f;', arr.length);

      // console.log('%c [  ]-271', 'font-size:13px; background:pink; color:#bf2c9f;', arr[1]);

      // for(const key in arr){

      //   console.log('%c [ key ]-271', 'font-size:13px; background:pink; color:#bf2c9f;', key);
      // }

    })

    effect(()=>{
      arr.push(1)
    })

    effect(()=>{
      arr.push(1)
    })



    // arr[1] = 'add'




  </script>
</body>

</html>